Consistency — it is not possible to read a stale value. Every read is guaranteed to read the latest data.
Inconsistent – it is possible for the reads to be stale (possible for us to read an old value)
Immediate Consistency — data will immediately be consistent. No waiting required. All copies sync up immediately. Impossible to read a stale/old value.
Eventual Consistency — data will be consistent eventually. For some time, the data might be inconsistent, but if you wait long enough, then it will become consistent (everything will sync up eventually)
No Consistency — data loss can happen! No matter how long you wait, your data will never become consistent.
More about consistency in future classes.
typical read/write cycle
Separation of concerns. Business logic required to fetch data (auth / sorting / filtering / ...) resides in the app server - it is a bad idea to have the same business logic in multiple places.
because it’s hard & slow — more on this later
When we do something like this, it is called a Write Through cache.
Why does a cache miss have to happen in the above manner?
If the cache doesn’t have some data, can’t it itself fetch it from the database, store it, and then return it to the app server?
disclaimer: some people do it, but it is typically considered bad design (anti-pattern)
Reason: separation of concerns
Therefore, any fetching/updating responsibilities lies with the app server. Cache server just acts as dumb-storage.
Note: since any writes only go the DB, the data inside the cache can get stale
If a read now comes to the cache, the cache will tell you happily that the value is 10 — which is incorrect!
Note that because this will be a cache hit, the app-server will just return this incorrect value to the client — it won’t even check the database.
note: you might also see TTL be called a cache eviction strategy — it is both an invalidation policy and an eviction policy (because it is so simple, but with a little modification. Pure TTL is NOT an eviction policy)
Whenever you store data inside cache, apart from storing the value, you also store the expiry time.
Every cache entry has a fixed amount of time for which it is valid (TTL).
TTL can be configured globally (for all keys), or it can be configured per key as well.
After the TTL expires, we assume that the value is stale, and we invalidate it.
In reality, we have no idea whether the value is actually stale or not (whether the value has been updated in the database or not)
For example, if we write a=10 to the cache at 8am, and our TTL is 1 hour, then the expiry for this entry will be set to 8am + 1 hour = 9am
Key | Value | Expiry |
a | 10 | 9 am |
b | 20 | 9.05 am |
Do we delete it
What if we read stale data from the cache, before the value has a chance to expire?
Yes, if an update happens in the database before the cache entry expires, then the subsequent reads (before expiry) will return the stale data from the cache.
So, TTL provides "eventual consistency"
Note: when the read request (after update) came, we first checked the cache (as we always do). Since we found the data in the cache, and the data had not expired (even though it was stale), we returned it to the user. We ended up serving stale data.
Completely dependent on the application.
It might be as low as 5 seconds, or it might be as large as a week.
The lower the TTL is, the more cache “misses” you will have, because the data will be invalidated very quickly. But at the same time, your data will be “more fresh”
The higher the TTL, the better your cache hit rate will be. But at the same time, you data will be “more stale”.
No, we should not do that when the read requests come
Imagine that the initial set in cache was done at 8.55 with expiry set to 9
An update was made in DB at 8.58, so the cache is now stale. And the cache will be invalidated at 9
If a read request comes at 8.59, and you move the expiry forward to 9.05, in this case your TTL is now higher — your data will remain stale until 9.05
We can push the expiry forward when we write to the cache, but then, it is not really TTL — it becomes something else entirely.
Imagine that you’re storing some fruits in your fridge..
TTL
very similar to TTL - as in it also provides "eventual consistency"
Key differences (compared to TTL)
user_preferences
user_id (bigint) name (text) preferences (json)
Find out all the users that are using dark theme on some mobile device.
select * from user_preferences up
join user_devices ud on ud.user_id = up.user_id
where ud.device_type = ‘mobile’ and up.theme = ‘dark’
group by up.user_id
Complex & slow query.
In this case, it might be better to use a Write Around policy.
In case of write around, there's a separate background process (running in a separate app server) that periodically fetches all recently updated data from DB, does the computation, and updates the cache in one go.
select * from ...
where updated_at > (now() - "1 hour")
Yes. Until the background "cron job" runs again, any updates to the db means that the cache has stale data.
Imagine that you’re storing some fruits in your fridge..
Write Around
Reads always check the cache. If the data is not in the cache, we simply return a 404 error to the client. We do NOT go to the database.
Why?
Because we use write-around when the data fetching/computation is expensive.
So if the computed data is not found in the cache, we want to wait for the computation to complete once, instead of doing the computation on the fly for each incoming request.
Write-around is typically done when the “computed” data is small enough to fit in the cache.
Eviction algorithm is usually not needed for the data that will be cached. You will evict it only when the data is no longer required.
For example, when the contest is over, you can evict the leaderboard data.
In certain cases, we require "immediate consistency" (aka strong consistency) (no stale reads, ever)
The only way to achieve that is to ensure that whatever writes happen, happen at both the cache and the db together!
Write through cache is difficult - because ensuring atomicity across multiple servers is difficult!
Single Server
Multiple Servers (cache & db)
2 Phase Commit - protocol used to perform atomic transactions in a distributed setting
We will formalize this trade-off in the CAP theorem class.
If you want consistency, you have to pay for it in blood (it will cause high latency and low throughput)
Note:
It provides no consistency guarantees. In fact, write back cache can lead to Data Loss!
Writes do NOT go to the database - instead, writes go to the cache
Periodically, you write the data back from the cache to the database
Yes, this can lead to data loss!
If the cache server crashes with unsynced changes, then the data is permanently lost.
Pros:
In scenarios where
If you’ve just started the cache server, it is initially empty.
Do you want your cache server to be empty?
No! You want the cache to be full, because otherwise you’re wasting much-needed cache. You want to cache as much data as possible!
As the operations continue, the cache server will get full — it will always remain full from this point onwards (unless it crashes & restarts)
But, you still want to cache more data into it (even though it is full) — this means, you first have to get rid of something that is already cached to make space for new data.
Cache Eviction algorithm decides which data to “kick-out” to make space for new data.
How do you decide which eviction policy to use?
99% cases
H/W: read about
Complete opposite of LRU.
Bad for 99% cases!
H/W: find a valid use-case of the MRU eviction policy